#include "Recipients.h"
#include <QUrl>
#include <QDataStream>
#include "Composer/Recipients.h"
#include "Imap/Model/ItemRoles.h"
#include "Imap/Model/Model.h"
#include "Imap/Model/MailboxTree.h"
#include "ReplyMode.h"

namespace Dekko
{

Recipient::Recipient(QObject *parent) : QObject(parent),
    m_type(Type::TO)
{
}

Recipient::Type Recipient::type() const
{
    return m_type;
}

void Recipient::setType(const Recipient::Type type)
{
    if (type != m_type) {
        m_type = type;
        emit typeChanged();
    }
}

QString Recipient::address() const
{
    return m_address;
}

void Recipient::setAddress(const QString &address)
{
    if (address != m_address) {
        m_address = address;
        Imap::Message::MailAddress::fromPrettyString(m_mailAddress, m_address);
        emit recipientChanged();
    }
}

QString Recipient::initials()
{
    QString name = this->name();
    if (name.isEmpty() || !name.at(0).isLetter()) {
        return QString();
    }
    // Intitials string
    QString initials;
    // Now break up the name, we have to set the encoding here as QT_NO_CAST_FROM/TO_ASCII is set
    QStringList parts = name.split(QStringLiteral(" "));
    if (parts.first().at(0).isLetter()) {
        initials += parts.first().at(0).toUpper();
    }
    if (parts.size() > 1) {
        if (parts.last().at(0).isLetter()) {
            initials += parts.last().at(0).toUpper();
        }
    }
    return initials;
}

QString Recipient::name()
{
    if (isInContacts()) {
        QString fname = contactInfo().value(QStringLiteral("firstName")).toString();
        QString lname = contactInfo().value(QStringLiteral("lastName")).toString();
        if (fname.isEmpty()) {
            // Do nothing
        } else if (!lname.isEmpty()) {
            return QStringLiteral("%1 %2").arg(fname, lname);
        } else {
            return fname;
        }
    }
    return m_mailAddress.prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME);
}

QString Recipient::email() const
{
    return m_mailAddress.prettyName(Imap::Message::MailAddress::FORMAT_JUST_EMAIL);
}

QVariantMap Recipient::contactInfo()
{
    if (isInContacts()) {
        return m_contacts->getByIndex(m_contacts->indexOfContactId(m_contactId));
    }
    return QVariantMap();
}

bool Recipient::isInContacts()
{
    // We just check to see if we have a contact id for this email address
    if (!m_contactId.isEmpty()) {
        return true;
    }
    m_contactId = m_contacts->getContactIdWithEmail(m_mailAddress.prettyName(Imap::Message::MailAddress::FORMAT_JUST_EMAIL));
    return !m_contactId.isEmpty();
}

Imap::Message::MailAddress Recipient::mailAddress() const
{
    return m_mailAddress;
}

void Recipient::setContactsModel(Contacts::ContactsModel *model)
{
    m_contacts = model;
}

void Recipient::addToContacts()
{
   m_contactId =  m_contacts->newContact(name(), QString(), QString(), QVariantList() << email());
   emit recipientChanged();
}

void Recipient::removeFromContacts()
{
    if (!m_contactId.isEmpty()) {
        m_contacts->removeContact(m_contactId);
    }
    emit recipientChanged();
}

Recipients::Recipients(QObject *parent) : QObject(parent), m_model(0)
{
    m_model = new QQmlObjectListModel<Recipient>(this);
    emit modelsChanged();
}

bool Recipients::buildRecipientList(const int mode, const QModelIndex &message)
{
    // first lets check this message is actually valid
    if (!message.isValid()) {
        return false;
    }
    clear();

    Composer::RecipientList allRecipients = getRecipientsFromMessage(message);
    QList<QUrl> listPost;
    Q_FOREACH(const QVariant &item, message.data(Imap::Mailbox::RoleMessageHeaderListPost).toList()) {
        listPost << item.toUrl();
    }

    return buildRecipientList(mode, allRecipients, listPost, message.data(Imap::Mailbox::RoleMessageHeaderListPostNo).toBool());
}

Composer::RecipientList Recipients::toRecipientList() const
{
    Composer::RecipientList recipientList;
    foreach(Recipient *r, m_model->toList()) {
        Composer::Recipient recipient;
        switch (r->type()) {
        case Recipient::TO:
            recipient.first = Composer::ADDRESS_TO;
            break;
        case Recipient::CC:
            recipient.first = Composer::ADDRESS_CC;
            break;
        case Recipient::BCC:
            recipient.first = Composer::ADDRESS_BCC;
            break;
        case Recipient::FROM:
        case Recipient::REPLY_TO:
        case Recipient::SENDER:
            break;
        default:
            recipient.first = Composer::ADDRESS_TO;
            break;
        }
        recipient.second = r->mailAddress();
        recipientList.append(recipient);
    }
    return recipientList;
}

void Recipients::setContacts(QObject *contacts)
{
    m_contacts = static_cast<Contacts::ContactsModel *>(contacts);
}

void Recipients::setIdentityModel(Accounts::MultipleAccountSenderIdentityModel *identities)
{
    m_identModel = identities;
}

bool Recipients::hasValidToDestination()
{
    foreach(Recipient *recipient, m_model->toList()) {
        if (recipient->type() == Recipient::TO) {
            return true;
        }
    }
    return false;
}

void Recipients::addRecipient(const int &type, const QString &address)
{
    if (address.isEmpty()) {
        return;
    }
    Recipient *r = new Recipient(this);
    r->setContactsModel(m_contacts);
    r->setAddress(address);
    r->setType((Recipient::Type)type);
    m_model->append(r);
}

void Recipients::clear()
{
    QStringList includes;
    includes << m_ccIncludes << m_bccIncludes;
    qDebug() << __FILE__ << __func__ << "Clearing recipients, except for: " << includes;
    if (!includes.isEmpty()) {
        foreach(Recipient *recipient, m_model->toList()) {
            if (!includes.contains(recipient->address())) {
                m_model->remove(recipient);
            }
        }
    } else {
        m_model->clear();
    }
}

void Recipients::setIncludes(const QStringList &includes, const bool isCc)
{
    auto removeOldAddNew = [=](const QStringList &oldList, const QStringList &newList) {
        // we need to remove the current and add the new ones
        Q_FOREACH(const QString &addr, oldList) {
            foreach(Recipient *recipient, m_model->toList()) {
                if (recipient->address() == addr) {
                    m_model->remove(recipient);
                }
            }
        }

        Q_FOREACH(const QString &addr, newList) {
            addRecipient(isCc ? Recipient::CC : Recipient::BCC, addr);
        }
    };

    if (isCc ) {
        if (includes == m_ccIncludes || !includes.size()) {
            return;
        }
        removeOldAddNew(m_ccIncludes, includes);
        m_ccIncludes = includes;
    } else {
        if (includes == m_bccIncludes || !includes.size()) {
            return;
        }
        removeOldAddNew(m_bccIncludes, includes);
        m_bccIncludes = includes;
    }
    qDebug() << __FILE__ << __func__ << "Includes set";
}

void Recipients::clearIncludes(const bool isCc)
{
    Q_FOREACH(const QString &addr, QStringList() << (isCc ? m_ccIncludes : m_bccIncludes)) {
        foreach(Recipient *recipient, m_model->toList()) {
            if (recipient->address() == addr) {
                m_model->remove(recipient);
            }
        }
    }
    if (isCc) {
        m_ccIncludes.clear();
    } else {
        m_bccIncludes.clear();
    }
}

Composer::RecipientList Recipients::getRecipientsFromMessage(const QModelIndex &message)
{
    Composer::RecipientList originalRecipients;
    if (!message.isValid())
        return originalRecipients;

    using namespace Imap::Mailbox;
    using namespace Imap::Message;

    Envelope envelope = message.data(Imap::Mailbox::RoleMessageEnvelope).value<Imap::Message::Envelope>();

    // Prepare the list of recipients
    Q_FOREACH(const MailAddress &addr, envelope.from)
        originalRecipients << qMakePair(Composer::ADDRESS_FROM, addr);
    Q_FOREACH(const MailAddress &addr, envelope.to)
        originalRecipients << qMakePair(Composer::ADDRESS_TO, addr);
    Q_FOREACH(const MailAddress &addr, envelope.cc)
        originalRecipients << qMakePair(Composer::ADDRESS_CC, addr);
    Q_FOREACH(const MailAddress &addr, envelope.bcc)
        originalRecipients << qMakePair(Composer::ADDRESS_BCC, addr);
    Q_FOREACH(const MailAddress &addr, envelope.sender)
        originalRecipients << qMakePair(Composer::ADDRESS_SENDER, addr);
    Q_FOREACH(const MailAddress &addr, envelope.replyTo)
        originalRecipients << qMakePair(Composer::ADDRESS_REPLY_TO, addr);

    return originalRecipients;
}

Composer::RecipientList Recipients::deduplicatedAndJustToCcBcc(Composer::RecipientList input)
{
    QList<Imap::Message::MailAddress> to, cc, bcc;

    Q_FOREACH(const Composer::Recipient &recipient, input) {
        switch (recipient.first) {
        case Composer::ADDRESS_TO:
            to << recipient.second;
            break;
        case Composer::ADDRESS_CC:
            cc << recipient.second;
            break;
        case Composer::ADDRESS_BCC:
            bcc << recipient.second;
            break;
        case Composer::ADDRESS_FROM:
        case Composer::ADDRESS_SENDER:
        case Composer::ADDRESS_REPLY_TO:
            // that's right, ignore these
            break;
        }
    }

    // Keep processing the To, Cc and Bcc fields, making sure that no duplicates (where comparing just the addresses) are present
    // in any of them and also making sure that an address is present in at most one of the (To, Cc, Bcc) groups.
    Composer::RecipientList result;
    QSet<QPair<QString, QString> > alreadySeen;

    Q_FOREACH(const Imap::Message::MailAddress &addr, to) {
        QPair<QString, QString> item = qMakePair(addr.mailbox, addr.host);
        if (!alreadySeen.contains(item)) {
            result << qMakePair(Composer::ADDRESS_TO, addr);
            alreadySeen.insert(item);
        }
    }

    Q_FOREACH(const Imap::Message::MailAddress &addr, cc) {
        QPair<QString, QString> item = qMakePair(addr.mailbox, addr.host);
        if (!alreadySeen.contains(item)) {
            result << qMakePair(result.isEmpty() ? Composer::ADDRESS_TO : Composer::ADDRESS_CC, addr);
            alreadySeen.insert(item);
        }
    }

    Q_FOREACH(const Imap::Message::MailAddress &addr, bcc) {
        QPair<QString, QString> item = qMakePair(addr.mailbox, addr.host);
        if (!alreadySeen.contains(item)) {
            result << qMakePair(Composer::ADDRESS_BCC, addr);
            alreadySeen.insert(item);
        }
    }
    return result;
}

Composer::RecipientList Recipients::mapRecipients(Composer::RecipientList input, const QMap<Composer::RecipientKind, Composer::RecipientKind> &mapping)
{
    Composer::RecipientList::iterator recipient = input.begin();
    while (recipient != input.end()) {
        QMap<Composer::RecipientKind, Composer::RecipientKind>::const_iterator operation = mapping.constFind(recipient->first);
        if (operation == mapping.constEnd()) {
            recipient = input.erase(recipient);
        } else if (*operation != recipient->first) {
            recipient->first = *operation;
            ++recipient;
        } else {
            // don't modify items which don't need modification
            ++recipient;
        }
    }
    return input;
}

bool Recipients::buildRecipientList(const int mode, const Composer::RecipientList &originalRecipients, const QList<QUrl> &headerListPost, const bool headerListPostNo)
{
    Composer::RecipientList newRecipientsList;
    switch (mode) {
    case ReplyMode::REPLY_ALL:
    {
        prepareReplyAll(originalRecipients, newRecipientsList);
        break;
    }
    case ReplyMode::REPLY_ALL_BUT_ME:
    {
        bool prepared = prepareReplyAll(originalRecipients, newRecipientsList);
        if (!prepared) {
            return false;
        }
        Composer::RecipientList tmp = newRecipientsList;
        Composer::RecipientList::iterator it = tmp.begin();
        while(it != tmp.end()) {
            Imap::Message::MailAddress addr = it->second;
            if (m_identModel->addressInIdentities(addr.prettyName(Imap::Message::MailAddress::FORMAT_JUST_EMAIL))) {
                it = tmp.erase(it);
            } else {
                it++;
            }
        }
        tmp = deduplicatedAndJustToCcBcc(tmp);

        // So we may have actually removed out cc/bcc includes so re-apply them
        // now
        if (!m_ccIncludes.isEmpty()) {
            setIncludes(m_ccIncludes, true);
        }
        if (!m_bccIncludes.isEmpty()) {
            setIncludes(m_bccIncludes, false);
        }

        newRecipientsList = tmp;
        break;
    }
    case ReplyMode::REPLY_LIST:
    {
        prepareReplyList(headerListPost, headerListPostNo, newRecipientsList);
        break;
    }
    case ReplyMode::REPLY_PRIVATE:
    {
        prepareReply(originalRecipients, headerListPost, newRecipientsList);
        break;
    }
    case ReplyMode::REPLY_NONE:
        break;
    }
    return appendRecipientsFromList(newRecipientsList);
}

bool Recipients::prepareReplyAll(const Composer::RecipientList &recipients, Composer::RecipientList &output)
{
    QMap<Composer::RecipientKind, Composer::RecipientKind> mapping;
    mapping[Composer::ADDRESS_FROM] = Composer::ADDRESS_TO;
    mapping[Composer::ADDRESS_REPLY_TO] = Composer::ADDRESS_CC;
    mapping[Composer::ADDRESS_TO] = Composer::ADDRESS_CC;
    mapping[Composer::ADDRESS_CC] = Composer::ADDRESS_CC;
    mapping[Composer::ADDRESS_BCC] = Composer::ADDRESS_BCC;
    Composer::RecipientList res = deduplicatedAndJustToCcBcc(mapRecipients(recipients, mapping));
    if (res.isEmpty()) {
        return false;
    } else {
        output = res;
        return true;
    }
}

bool Recipients::prepareReply(const Composer::RecipientList &recipients, const QList<QUrl> headerListPost, Composer::RecipientList &output)
{
    // Create a blacklist for the Reply-To filtering. This is needed to work with nasty mailing lists (hey, I run quite
    // a few like that) which do the reply-to munging.
    QList<QPair<QString, QString> > blacklist;
    Q_FOREACH(const QUrl &url, headerListPost) {
        if (url.scheme().toLower() != QLatin1String("mailto")) {
            // non-mail links are not relevant in this situation; they don't mean that we have to give up
            continue;
        }

        QStringList list = url.path().split(QLatin1Char('@'));
        if (list.size() != 2) {
            // Malformed mailto: link, maybe it relies on some fancy routing? Either way, play it safe and refuse to work on that
            return false;
        }

        blacklist << qMakePair(list[0].toLower(), list[1].toLower());
    }

    // Now gather all addresses from the From and Reply-To headers, taking care to skip munged Reply-To from ML software
    Composer::RecipientList originalFrom, originalReplyTo;
    Q_FOREACH(const Composer::Recipient &recipient, recipients) {
        switch (recipient.first) {
        case Composer::ADDRESS_FROM:
            originalFrom << qMakePair(Composer::ADDRESS_TO, recipient.second);
            break;
        case Composer::ADDRESS_REPLY_TO:
            if (blacklist.contains(qMakePair(recipient.second.mailbox.toLower(), recipient.second.host.toLower()))) {
                // This is the safe situation, this item in the Reply-To is set to a recognized mailing list address.
                // We can safely ignore that.
            } else {
                originalReplyTo << qMakePair(Composer::ADDRESS_TO, recipient.second);
            }
            break;
        default:
            break;
        }
    }

    if (!originalReplyTo.isEmpty()) {
        // Prefer replying to the (ML-demunged) Reply-To addresses
        output = originalReplyTo;
        return true;
    } else if (!originalFrom.isEmpty()) {
        // If no usable thing is in the Reply-To, fall back to anything in From
        output = originalFrom;
        return true;
    } else {
        // No recognized addresses -> bail out
        return false;
    }
}

bool Recipients::prepareReplyList(const QList<QUrl> &headerListPost, const bool headerListPostNo, Composer::RecipientList &output)
{
    if (headerListPostNo)
        return false;

    Composer::RecipientList res;
    Q_FOREACH(const QUrl &url, headerListPost) {
        if (url.scheme().toLower() != QLatin1String("mailto"))
            continue;

        QStringList mail = url.path().split(QLatin1Char('@'));
        if (mail.size() != 2)
            continue;

        res << qMakePair(Composer::ADDRESS_TO, Imap::Message::MailAddress(QString(), QString(), mail[0], mail[1]));
    }

    if (!res.isEmpty()) {
        output = deduplicatedAndJustToCcBcc(res);
        return true;
    }

    return false;
}

bool Recipients::appendRecipientsFromList(const Composer::RecipientList &recipients)
{
    Q_FOREACH(const Composer::Recipient &r, recipients) {
        Recipient::Type type;
        switch (r.first) {
        case Composer::ADDRESS_TO:
            type = Recipient::TO;
            break;
        case Composer::ADDRESS_CC:
            type = Recipient::CC;
            break;
        case Composer::ADDRESS_BCC:
            type = Recipient::BCC;
            break;
        case Composer::ADDRESS_FROM:
            type = Recipient::FROM;
            break;
        case Composer::ADDRESS_REPLY_TO:
            type = Recipient::REPLY_TO;
        case Composer::ADDRESS_SENDER:
            type = Recipient::SENDER;
            break;
        default:
            type = Recipient::TO;
            break;
        }
        addRecipient(type, r.second.asPrettyString());
    }
    return true;
}

RecipientsFilterModel::RecipientsFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
}

void RecipientsFilterModel::setSource(QAbstractItemModel *source)
{
    m_model = static_cast<QQmlObjectListModel<Recipient> *>(source);
    setSourceModel(m_model);
    emit sourceChanged();
}

int RecipientsFilterModel::getDeproxifiedIndex(QVariant obj)
{
    Recipient *recipient = qobject_cast<Recipient *>(qvariant_cast<QObject *>(obj));
    Q_ASSERT(recipient);
    return m_model->indexOf(recipient);
}

bool RecipientsFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{

    QModelIndex index = m_model->index(source_row, 0, source_parent);

    if (index.isValid()) {
        Recipient *recipient = qobject_cast<Recipient *>(qvariant_cast<QObject *>(m_model->data(index, Qt::UserRole)));
        Q_ASSERT(recipient);
        if (!recipient) {
            return false;
        }
        if (filterRole() == FILTER_TO) {
            return recipient->type() == Recipient::TO;
        } else if (filterRole() == FILTER_CC) {
            return recipient->type() == Recipient::CC;
        } else if (filterRole() == FILTER_BCC) {
            return recipient->type() == Recipient::BCC;
        }
    }
    return false;
}

}
